{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 程序的基本结构（五）：异常处理"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "异常处理是个很重要，也有点难度的概念。我们先尝试理解基本的概念，后面再不断通过实践来加深掌握。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们前面说过，大多数程序的工作都是：接受输入，对输入进行处理，然后输出结果。这里有个很重要的理念，是所有资深程序员都习惯成自然的认知，那就是“输入是不可控的”，输入可能是用户通过键盘鼠标触控屏输入的，也可能是读取某个设备上一个程序的输出，简言之，都存在不可期的情况。\n",
    "\n",
    "假定我们期待用户或者某个程序给我们一个不为 0 的整数，我们要拿来做除数算出一个值，如果来的不是整数是个小数怎么办？或者就是个 0 怎么办？或者干脆就不是个数怎么办？\n",
    "\n",
    "还有依赖于外部设备的各种异常状况：如果我们从某个程序读一个数，但是那个程序死掉了，一直没给我们数，我们一直等着卡死在这里？或者我们要向打印机输出一个报表，但是打印机被人踢掉了电源线，一直不响应怎么办？"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "所有这些错误都在程序运行时才会出现，程序写出来是没有错的，运行时出现奇怪的异常状况，程序又没有好好处理的话，就会出现灾难，这些运行时出现的异常状况就叫**运行时错误**（*runtime error*）或者**运行时异常**（*runtime exception*）。现代编程语言一般都提供标准的异常处理方案，让我们可以写程序来处理这类异常。\n",
    "\n",
    "Python 提供的异常处理机制可以用下面的模板来说明：\n",
    "\n",
    "```python\n",
    "try:\n",
    "    # 把有可能出现异常的代码放在 try 后面\n",
    "    # 当出现异常时解释器会捕获异常\n",
    "    # 并根据异常的类型执行后面的对应代码块\n",
    "    do_something_nasty()\n",
    "except ValueError:\n",
    "    # 如果发生 ValueError 类型的异常则执行这个代码块\n",
    "    pass\n",
    "except (TypeError, ZeroDivisionError):\n",
    "    # 可以一次指定几个不同类型的异常在一起处理exceptions\n",
    "    # 如果出现 TypeError 或者 ZeroDivisionError 则执行这个代码块\n",
    "    pass\n",
    "except:\n",
    "    # 所有上面没有专门处理的类型的异常会在这里处理\n",
    "    pass\n",
    "else:\n",
    "    # 当且仅当 try 代码块里无异常发生时这个代码块会被执行\n",
    "    pass\n",
    "finally:\n",
    "    # 无论发生了什么这个代码块都会被执行\n",
    "    # 通常这里是清理性的代码，比如我们在 try 里面打开一个文件进行处理\n",
    "    # 无论过程中有没有异常出现最后都应该关闭文件释放资源\n",
    "    # 这样的操作就适合在这里执行\n",
    "```\n",
    "\n",
    "上面出现的关键字 `pass` 的意思是“什么也不做”，Python 语法需要有点什么，但是我们暂时什么都不想做的时候放上一个 `pass` 就可以了。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "以 `try` 开始的异常处理结构可以包含所有这些模块，但并不是都必须有，但至少应该有一个 `except` 或者 `finally`。\n",
    "\n",
    "Python 有很多[内置的运行时错误类型](https://docs.python.org/3/library/exceptions.html#bltin-exceptions)可以直接使用，比如我们上面看到的：\n",
    "* `TypeError`：当一个操作或者函数收到的参数类型不对，或者一个类型的对象不支持某个被请求的操作时抛出这个异常；\n",
    "* `ValueError`：当一个操作或者函数收到的参数类型对但是值不合法时抛出这个异常；\n",
    "* `ZeroDivisionError`：出现 0 作除数的情况时抛出这个异常。\n",
    "\n",
    "我们来看几个官方指引中的例子并稍作解释。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdin",
     "output_type": "stream",
     "text": [
      "Please enter a number:  123g\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Not a valid number. Try again...\n"
     ]
    },
    {
     "name": "stdin",
     "output_type": "stream",
     "text": [
      "Please enter a number:  42\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Your number is: 42\n"
     ]
    }
   ],
   "source": [
    "while True:\n",
    "    try:\n",
    "        x = int(input('Please enter a number: '))\n",
    "        break\n",
    "    except ValueError:\n",
    "        print('Not a valid number. Try again...')\n",
    "\n",
    "print('Your number is:', x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这里用一个无限循环中的 `input()` 方法来获取用户输入，然后把输入的字符串用 `int()` 方法转换为一个整数；如果用户没有输入一个整数，`int()` 方法会抛出一个 `ValueError`，于是执行 `except ValueError` 后面的代码块，打印一个提示，然后继续 `while True`，再次提示用户输入；如果 `try` 代码段里第一句执行成功（用户输入成功转换为整数并赋值给 x 变量），那就继续执行后面一句 `break` 终止循环，继续执行其他代码（这时候 x 里面已经有了用户输入的整数）。\n",
    "\n",
    "在这里异常处理确保用户输入可以转换为整数且赋值给 x，否则就不会继续执行下去，经过这样的处理，在这段代码之后我们可以相当有把握的说：x 里面有个合法的、用户输入的整数值；同时用户不管怎么乱输入也不会对程序构成致命影响，我们预期到可能出现的问题，并做了合理处理。这就是异常处理的意义所在。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Handling run-time error: division by zero\n"
     ]
    }
   ],
   "source": [
    "def this_fails():\n",
    "    x = 1/0\n",
    "\n",
    "try:\n",
    "    this_fails()\n",
    "except ZeroDivisionError as err:\n",
    "    print('Handling run-time error:', err)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这个例子展示了用 `except ZeroDivisionError as err` 这样的语法来取得一个 `err` 对象，这个对象是系统定义的 `Exception` 类型或者子类，里面存放着发生异常时的具体上下文信息，可以打印出来也可以做别的处理。\n",
    "\n",
    "我们还可以从 `Exception` 派生出我们自己的异常类型，并使用 `raise` 关键字来在出现某种情况时抛出我们定义的异常，并在文档中做出清晰的说明。这样使用我们代码的其他程序员就知道什么情况是我们程序处理不了的，会抛出什么样的异常，并在调用端用捕获异常进行处理。\n",
    "\n",
    "建议学习 [关于 Python 异常处理的官方教程](https://docs.python.org/3/tutorial/errors.html) 来了解更多。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 小结"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* 程序处理用户或其他系统提供的输入时可能出现预期之外的异常状况，可以使用异常处理来捕获异常并进行应急处置；\n",
    "* 理解 Python 异常处理的模板含义；\n",
    "* 通过例子初步了解异常处理可能的应用场景。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
